package com.googlecode.cswish.struts.conversion;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.net.URL;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

import javax.annotation.Resource;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.struts2.ServletActionContext;
import org.apache.struts2.StrutsConstants;
import org.apache.struts2.views.freemarker.FreemarkerManager;
import org.apache.struts2.views.freemarker.ScopesHashModel;
import org.springframework.stereotype.Service;

import com.googlecode.cswish.Config;
import com.googlecode.cswish.model.PageElements;
import com.googlecode.cswish.struts.BaseActionSupport;
import com.googlecode.cswish.struts.Invoker;
import com.googlecode.cswish.struts.Routing;
import com.googlecode.cswish.struts.spring.BeanIntrospector;
import com.googlecode.cswish.struts.spring.ObjectFactory;
import com.googlecode.cswish.test.mock.struts2.MockValueStack;
import com.googlecode.cswish.test.mock.struts2.jsp.StrutsMockHttpServletRequest;
import com.googlecode.cswish.test.mock.struts2.jsp.StrutsMockHttpServletResponse;
import com.googlecode.cswish.util.ClassUtil;
import com.googlecode.cswish.util.FrameConstant;
import com.googlecode.cswish.util.Functions;
import com.opensymphony.xwork2.ActionContext;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.config.ConfigurationException;
import com.opensymphony.xwork2.config.entities.PackageConfig;
import com.opensymphony.xwork2.config.entities.ResultTypeConfig;
import com.opensymphony.xwork2.inject.Container;
import com.opensymphony.xwork2.inject.Inject;
import com.opensymphony.xwork2.util.ValueStack;
import com.opensymphony.xwork2.util.logging.Logger;
import com.opensymphony.xwork2.util.logging.LoggerFactory;

import freemarker.template.Configuration;
import freemarker.template.Template;
import freemarker.template.TemplateModelException;

/**
 * Wrapper struts FreeMarker manager, @see StrutsFreemarkerManager
 * 
 * @author Jerry.Feng
 */
@Service
public class StrutsFreemarkerManager {

	protected static Logger logger = LoggerFactory.getLogger(StrutsFreemarkerManager.class);
	
	@Resource
	private ObjectFactory objectFactory;
	
	@Resource
	private Routing routing;
	
	@Resource
	private Config config;
	
	@Resource
	private PageConverter pageConverter;
	
	@Resource
	private Functions functions;
	
	@Resource
    private BeanIntrospector beanIntrospector;
	
	@Inject
	private FreemarkerManager freemarkerManager;
	
	@Inject
	private ServletContext servletContext;
	
	@Inject
	private Container container;
	
	@Inject
	private com.opensymphony.xwork2.config.Configuration configuration;
	
	@Inject("struts.cswish.defaultPackage")
	private String defaultPackageName;
	
	@Inject(StrutsConstants.STRUTS_ACTION_EXTENSION)
	private String actionExtension;
	
	private LinkedHashMap<String, ResultTypeConfig> resultsByExtension;
	
	public String UNDEFINED_PAGE;
	public String PREPARING_PAGE;
	
	// @PostConstruct
	protected void init() {
		objectFactory.injectInternalBeans(this);
		
		resultsByExtension = new LinkedHashMap<String,ResultTypeConfig>();
		PackageConfig parentPackage = configuration.getPackageConfig(defaultPackageName);
        if (parentPackage == null) {
            throw new ConfigurationException("Unknown parent package: " + parentPackage);
        }    
        Map<String,ResultTypeConfig> results = parentPackage.getAllResultTypeConfigs();

        // template list
        resultsByExtension.put("ftl", results.get("freemarker"));
        resultsByExtension.put("html", results.get("freemarker"));
        resultsByExtension.put("jsp", results.get("dispatcher"));
        //resultsByExtension.put("htm", results.get("freemarker"));
        //resultsByExtension.put("vm", results.get("velocity"));
        
        UNDEFINED_PAGE = config.getCommonViewPkg() + "page.undefined.ftl";
        PREPARING_PAGE = config.getCommonViewPkg() + "website.status.ftl";
	}
	
	public String executeTpl(String actionPath, PageElements pageElements, Map<String, Object> parameters) {
		return executeTpl(actionPath, true, pageElements, parameters);
	}
	
	public String executeTpl(String pathWithSite, boolean isAtionPath,  PageElements pageElements, Map<String, Object> parameters) {
		if (freemarkerManager == null) {
			init();
		}
		Configuration configuration = freemarkerManager.getConfiguration(servletContext);
		ActionContext context = ActionContext.getContext();
		ActionInvocation invocation = context == null ? null : context.getActionInvocation();
		Map<String, Object> invocationContext = invocation == null ? null : invocation.getStack().getContext();
		PageElements originalPageElements = invocationContext == null ? null : (PageElements) invocationContext.get(FrameConstant.STACK_PAGEELEMENTS);
		try {
			if (invocationContext != null) {
				invocationContext.put(FrameConstant.STACK_PAGEELEMENTS, pageElements);
			}
			
			String location = isAtionPath ? findLocation(pathWithSite) : pathWithSite;
			Template templateInstance = configuration.getTemplate(location, Locale.ENGLISH);
			ScopesHashModel model = createModel(invocation, pathWithSite, pageElements, parameters, configuration);
			CharArrayWriter charArrayWriter = new CharArrayWriter();
			templateInstance.process(model, charArrayWriter);
			
			return charArrayWriter.toString();
		} catch (Exception ex) {
			logger.error("Failed to populate the elements", ex);
		} finally {
			if (invocationContext != null) {
				invocationContext.put(FrameConstant.STACK_PAGEELEMENTS, originalPageElements);
			}
		}
		return null;
	}
	
	public Template getTemplate(String templateLocation) {
		if (freemarkerManager == null) {
			init();
		}
		Configuration configuration = freemarkerManager
				.getConfiguration(ServletActionContext.getServletContext());
		try {
			Template templateInstance = configuration.getTemplate(templateLocation, Locale.ENGLISH);
			return templateInstance;
		} catch (Exception ex) {
			logger.error("Failed to load template: " + templateLocation, ex);
		}
		return null;
	}
	
	public void removeTemplateFromCache(String ftlPath) {
		if (freemarkerManager == null) {
			init();
		}
		Configuration configuration = freemarkerManager
				.getConfiguration(ServletActionContext.getServletContext());
		try {
			configuration.removeTemplateFromCache(ftlPath);
		} catch (IOException ex) {
			logger.error("Failed to remove template: " + ftlPath, ex);
		}
	}
	
	private ScopesHashModel createModel(ActionInvocation invocation, String pathWithSite, PageElements pageElements, Map parameters, Configuration configuration) throws TemplateModelException {
		ScopesHashModel model;
		if (invocation == null) {		// maybe, the request comes from a new thread
			StrutsMockHttpServletRequest request = new StrutsMockHttpServletRequest();
			request.setupGetContext(servletContext.getContextPath());
			HttpServletResponse response = new StrutsMockHttpServletResponse();
			ValueStack stack = new MockValueStack(container);

			Map<String, Object> stackContext = stack.getContext();
			stackContext.put(FrameConstant.STACK_PAGEELEMENTS, pageElements);
			stackContext.put("action", parameters);
			Invoker invoker = routing.path2Invoker(pathWithSite, true);
			stackContext.put(FrameConstant.STACK_ACTIONNAME, invoker.actionName);
			stackContext.putAll(parameters);
			
			model = freemarkerManager.buildTemplateModel(stack, parameters, servletContext, request, response, configuration.getObjectWrapper());
		} else {
	        ServletContext servletContext = ServletActionContext.getServletContext();
	        HttpServletRequest request = ServletActionContext.getRequest();
	        HttpServletResponse response = ServletActionContext.getResponse();
	        ValueStack stack = invocation.getStack(); 
	        if (stack instanceof MockValueStack) {
	        	ValueStack realStack = ServletActionContext.getContext().getValueStack();
	        	stack.getContext().putAll(realStack.getContext());
	        }
	         
	        
	        Object action = stack.findValue("action");
	        if (action == null) {
	        	stack.getContext().put("action", new BaseActionSupport());
	        } else if (action instanceof Map) {
	        	// TODO: back up the original value?
	        	((Map)action).putAll(parameters);
	        } else {
	        	// TODO: use stack.set?
	        }
	        model = freemarkerManager.buildTemplateModel(stack, parameters, servletContext, request, response, configuration.getObjectWrapper());
		}
        return model;
    }
	
	private boolean checkPath(String path) {
		URL url = null;
		try {
			url = servletContext.getResource(path);
        	if (url == null) {
        		if (path.charAt(0) == '/') {
        			url = ClassUtil.getResource(path.substring(1), getClass());
        		} else {
        			url = ClassUtil.getResource(path, getClass());
        		}
            }
        } catch (Exception ex) {
        	if (logger.isDebugEnabled()) {
        		logger.debug("check resource:" + path, ex);
        	}
        }
        return url != null ? true : false;
    }

	public String findLocation(String context) {
		// use the method name
		Invoker invoker = routing.path2Invoker(context, true);
		return _findLocation(context, invoker);
	}
	
	// Notice: the method is low performance, it check lots of file path
	// Only used by CSwishUnknownHandler
	public String findLocation(String context, Invoker invoker) {
		String location = null;
		try {
			// FIXME: correct? check the site status at the entrance?
			location = _findLocation(context, invoker);
		} catch (Exception ex) {
			if (logger.isDebugEnabled()) {
				logger.debug("Can't find location:" + context, ex);
			}
			location = PREPARING_PAGE;
		}
		return location;
	}
	
	private String _findLocation(String context, Invoker invoker) {
		if (freemarkerManager == null) {
			init();
		}
		
		// 1. search from the default folder (don't search template file, because the template id is dynamic)
		String simpleName = beanIntrospector.getSimpleName(invoker.actionName);
		String methodName = invoker.methodName;
		String path = findRealPath(simpleName, invoker.separator, methodName);
				
		// search the generated code
		if (path == null) {
			// TODO: correct? get path from context holder
			if (pageConverter.hasPage(simpleName, methodName)) {
				StringBuilder sb = new StringBuilder();
				sb.append(config.getSitePath());
				String pathNoExt = Routing.invoker2PathNoExtension(simpleName, invoker.separator, methodName, false);
				sb.append(config.getViewDir4GenCode()).append(pathNoExt);
				sb.append(".ftl");
				path = sb.toString();
			}
		}
		
		// 3. search from the template folder
		//URL res = ClassLoaderUtils.getResource(path, this.getClass());		
		if (path == null) {
			for (Iterator<String> iterator = resultsByExtension.keySet().iterator(); iterator.hasNext();) {
				String suffix = iterator.next();
				// use the generic template
				String checkPath = config.getCommonViewPkg() 
					+ "generic." + methodName + '.' + suffix;
				
				if (checkPath(checkPath)) {
					path = checkPath;
					break;
				}
			}
		}
		
		// 3. give the developer a tip information
		if (path == null) {
			// TODO: use better way to resolve the hard code logic: 
			// move it to Routing, SafeManager?
			logger.warn("Can't find the view for action: " + invoker);
			boolean isLoginPage = context.endsWith("eap/sa/user._login");
			if (isLoginPage) {
				path = config.getViewDir() + "espirit/eap/sa/user._login.html";
			} else {
				path = config.getCommonViewPkg() + "page.undefined.ftl";
			}
		}
		return path;
	}

	private String findRealPath(String simpleName, char separator, String methodName) {
		String path = null;
		boolean found = false;
		String pathPrefix = config.getViewDir() + Routing.invoker2PathNoExtension(simpleName,
				separator, methodName, false) + '.';
		for (Iterator<String> iterator = resultsByExtension.keySet().iterator(); iterator.hasNext();) {
			String suffix = iterator.next();
			path = pathPrefix + suffix;
			
			if (checkPath(path)) {
				found = true;
				break;
			}
		}
		
		// 2. skip the method and search again
		if (!found) {
			pathPrefix = config.getViewDir() + simpleName.replace('.', '/') + '.';
			
			for (Iterator<String> iterator = resultsByExtension.keySet().iterator(); iterator.hasNext();) {
				String suffix = iterator.next();
				path = pathPrefix + suffix;
				
				if (checkPath(path)) {
					found = true;
					break;
				}
			}
		}
		return found ? path : null;
	}
}